<?php
/*
 * Glorious Framework
 *
 * Basic Form processing class
 *
 * (c) 2007-2010 Konspire Design
 *
 * $Id: form.php,v 9005ae729cad 2010/05/07 12:20:17 and $
 * $Author: and $
 * $Date: 2010/05/07 12:20:17 $
*/

abstract class basic_Formx {

	protected $field_template = "<div:divatts>\n:before:element\n:inline-error\n:after\n</div>:extra\n\n";
	protected $fields = array();
	protected $buttons = array();
	protected $default_classes = array(
			'text' => 'textbox',
			'password' => 'textbox',
			'file' => 'textbox',
			'textarea' => 'textarea',
			'multiselect' => 'select',
			'select' => 'select',
			'radio' => 'radio',
			'checkbox' => 'checkbox',
			'reset' => 'button',
			'submit' => 'button',
			'button' => 'button',
			'_div' => 'form-field'
	);

	protected $validator;

	protected $nonce_action = '';
	protected $url = '';
	protected $form_prefix = '';
	protected $form_method = 'post';
	protected $multipart = false;

	protected $inline_errors = false;
	protected $compiled = false;

	protected $data = array();
	protected $post_data = null;

	public function __construct($url = '', $nonce_action = null, $inline_errors = null, $method = null) {
		$this->validator = new Form_validator();

		$this->url($url);

		$this->nonce_action($nonce_action);

		if( !is_null( $inline_errors ) )
			$this->inline_errors($inline_errors);
		
		if( !is_null( $method ) )
			$this->form_method = strtolower($method) == 'get' ? 'get' : 'post';
	}

	public function default_class($name, $value = false) {

		$dc =& $this->default_classes;

		if($value === false) {
			if(isset($dc[$name]))
				$value = $dc[$name];
		} else
			$dc[$name] = $value;

		return $value;
	}

	public function url($url = false) {
		
		if($url !== false)
			$this->url = $url;

		return $this->url;
	}

	public function is_posted() {

		if($this->form_method == 'post') {
			return sizeof($_POST) > 0;
		}


		return true; // it always true when get
	}

	public function nonce_action($n = false) {

		if(is_null($n)) {
			$this->form_prefix = $this->normalize_attr($this ->nonce_action);
		} elseif($n !== false) {
			$this->nonce_action = $n;
			$this->form_prefix = $this->normalize_attr($n);
		}

		return $this->nonce_action;
	}

	public function set_validator_error($validator, $str) {
		$this->validator->set_validator_error($validator, $str);
	}

	public function inline_errors($e = null) {
		if(!is_null($e))
			$this->inline_errors = $e;

		return $this->inline_errors;
	}

	public function bind_data($name, $data) {
		$this->data[$name] = $data;
	}

	public function set_values($data) {
		$this->_set_values($data, $this->fields);
	}

	protected function _set_values($data, &$where) {

		foreach($data as $key => $value) {

			if(isset($where[$key])) {

				if(is_array($value)) {

					if(isset($where[$key]['elements']))
						$this->_set_values($value, $where[$key]['elements']);
					else
						$where[$key]['value'] = $value;

				} else {
					$where[$key]['value'] = $value;
				}

			}

		}
		
	}

	public function get_data($name, $default = false) {
		if(isset($this->data[$name])) {
			return $this->data[$name];
		}

		return $default;
	}

	public function unbind_data($name) {
		if(isset($this->data[$name])) {
			unset($this->data[$name]);
			return true;
		}

		return false;
	}

	protected function bind_vars(&$val) {

		$e = $val;
		
		if(is_string($e) && strpos($e, ':') === 0 && strlen($e) > 0) {
			$e = substr($e, 1);
			
			if(isset($this->data[$e])) {
				$val = $this->data[$e];
			} else {
				$val = '';
			}
		}

	}

	public function set_fields($fields) {
		if(is_string($fields)) {
			$fields = json_decode($data);
		}

		$this->fields = $fields;

		return $this;
	}

	public function fields() {
		return $this->fields;
	}

	public function set_field_template($templ) {
		$this->field_template = $templ;
	}

	public function field_template() {
		return $this->field_template;
	}

	public function get_postdata( $k = null ) {

		$r = null;

		if(is_null($this->post_data))
			$this->prepare_postdata();

		if(!is_null($k)) {
			if(isset($this->post_data[$k]))
				$r = $this->post_data[$k];
		} else
			$r = $this->post_data;
		
		return $r;
	}

	protected function normalize_values($values) {
		if(is_object($values))
			return (array)$values;

		if(!is_array($values))
			return array($values);

		return $values;
	}

	protected function normalize_attr($str) {
		if( !isset( $str ) ) return null; // dont touch null
		return str_replace(array('-', '['), '_', preg_replace( '#[^a-z0-9_\[-]+#i', '', $str));
	}

	public function format_errors($format = '', $field = '') {
		return $this->validator->format_errors($format, $field);
	}

	protected function set_validation_recursive($prefix, $fields) {

		foreach($fields as $field => $options) {

			$real_field_name = $field;

			if(!empty($prefix))
				$real_field_name = $prefix . '[' . $field . ']';

			if($this->is_numeric_array(array_keys($options))) {
				$this->set_validation_recursive($real_field_name, $options);
			}
			else if(isset($options['elements'])) {
				$this->set_validation_recursive($real_field_name, $options['elements']);
			}
			else if(isset($options['validation'])) {

				if(isset($options['caption']))
					$caption = $options['caption'];
				else
					$caption = !empty($options['label']) ? $options['label'] : $field;

				$this->validator->set_rules($real_field_name, $caption, $options['validation']);
			}

		}

	}

	protected function _compile($form_path, &$fields) {
		$keys = array_keys($fields);

		for($i = 0; $i < sizeof($keys); $i++) {

			$field_index = $keys[$i];
			$field_data =& $fields[$field_index];
			
			$form_rel_name = $field_index;
			if(!empty($form_path))
				$form_rel_name = $form_path . '[' . $form_rel_name . ']';

			//var_dump($form_rel_name);

			if(isset($field_data['value'])) {
				$this->bind_vars($field_data['value']);
			}

			if(isset($field_data['list']))
				$this->bind_vars($field_data['list']);

			// save post data into the form
			// if($this->is_posted())
			$test = form_value($form_rel_name, false, $this->form_method);

			if($test !== false) {
				$field_data['value'] = $test;
			}

			
			// compile sets
			if(isset($field_data['type'], $field_data['elements']) && $field_data['type'] == 'set')
			{

				$elements_copy = $field_data['elements'];

				// copy data from set into elements
				if(isset($field_data['value']) && 
						is_array($field_data['value'])) {
					
					$v = $field_data['value'];

					foreach($v as $index => $data) {
						if(isset($elements_copy[$index])) {
							$elements_copy[$index]['value'] = $data;
						}
					}

				}

				$field_data['elements'] = array();
				$new_elements =& $field_data['elements'];

				$set_size = $this->explain_set_size($elements_copy);

				$field_data['set_size'] = $set_size;

				for($j = 0; $j < $set_size; $j++) {

					foreach($elements_copy as $index => $data) {

						$gen_element_name = $form_rel_name . '[' . $index . '][' . $j . ']';

						if(isset($data['elements']))
							$this->_compile($gen_element_name, $data['elements']);

						if(!isset($new_elements[$index]))
							$new_elements[$index] = array();

						if(isset($data['value']))
							$this->bind_vars($data['value']);

						if(isset($data['list']))
							$this->bind_vars($data['list']);

						if(isset($data['value']) && is_array($data['value']) && sizeof($data['value']) > $j){
							$data_keys = array_keys($data['value']);

							$data['value'] = $data['value'][$data_keys[$j]];
						}

						$new_elements[$index][] = $data;

					}

				}

			} else if(isset($field_data['elements'])) {
				$this->_compile($form_rel_name, $field_data['elements']);
			}

		}
	}

	public function compile() {
		$this->compiled = true;
		$this->_compile('', $this->fields);
	}

	protected function prepare_postdata() {

		$this->post_data = array();

		$raw = $this->form_method == 'get' ? $_GET : $_POST;

		if(!empty($this->form_prefix) && isset($raw[$this->form_prefix])) {
			$raw = $raw[$this->form_prefix];
		}

		$this->post_data = $this->_prepare_postdata($this->fields, $raw);
	}

	protected function _prepare_postdata($fields, $raw) {
		
		$result = array();
		$keys = array_keys($fields);

		foreach($fields as $field => $options) {

			if($this->is_numeric_array(array_keys($options))) {
				if(isset($raw[$field])) {
					$ret = $this->_prepare_postdata($options, $raw[$field]);
					$result = array_merge($result, $ret);
				}
			}
			else if(isset($raw[$field])) {
				$result[$field] = $raw[$field];
			}
		}

		return $result;
	}

	public function validate() {

		if(!$this->is_posted())
			return false;

		// reset validator errors and rules
		$this->validator->reset();

		// compile form if it hasn't been compiled before
		if(!$this->compiled)
			$this->compile();

		$this->set_validation_recursive('', $this->fields);

		$ret = $this->validator->run($this->nonce_action);

		$this->prepare_postdata();

		return $ret;
	}

	protected function explain_set_size($elements) {

		$max = 0;

		foreach($elements as $field => $options) {
			$emax = 0;

			if(isset($options['value'])) {
				$emax = is_array($options['value']) ? sizeof($options['value']) : 1;
			} else if(isset($options['elements'])) {
				$emax = $this->explain_set_size($options['elements']);
			}

			if($emax > $max)
				$max = $emax;

		}

		return $max;

	}

	public function get_real_fieldname($field_name) {
		$form_prefix = $this->normalize_attr($this->nonce_action);
		if($form_prefix != '')
			return preg_replace('#^(\w+)(.*?)$#i', preg_quote($form_prefix) . '[$1]$2', $field_name);

		return $field_name;
	}

	protected function is_numeric_array($arr) {
		foreach($arr as $val) {
			if(!is_numeric($val))
				return false;
		}

		return true;
	}

	protected function render_elements($form_path, $fields) {
		$form = '';

		foreach($fields as $field => $options) {

			if($this->is_numeric_array(array_keys($options))) {
				$form_rel_name = $field;
				if(!empty($form_path))
					$form_rel_name = $form_path . '[' . $form_rel_name . ']';

				$form .= $this->render_elements($form_rel_name, $options);
			}
			else {
				$form .= $this->render_element($form_path, $field, $options);
			}
		}

		return $form;
	}

	protected function render_label($field, $input_atts) {
		$text = '';

		unset($input_atts['type'], $input_atts['name']);

		if(isset($input_atts['value']))
			$text = $input_atts['value'];

		unset($input_atts['value']);

		return form_label($text, $input_atts);
	}

	protected function render_text($field, $input_atts) {
		return $this->_render_input($field, $input_atts);
	}

	protected function render_password($field, $input_atts) {
		return $this->_render_input($field, $input_atts);
	}

	protected function render_hidden($field, $input_atts) {
		return $this->_render_input($field, $input_atts);
	}

	protected function render_file($field, $input_atts) {
		$this->multipart = true;

		return $this->_render_input($field, $input_atts);
	}

	protected function _render_input($field, $input_atts) {
		$input_atts['value'] = form_value($input_atts['name'], isset($input_atts['value']) ? $input_atts['value'] : '', $this->form_method);

		return form_input($input_atts);
	}

	protected function render_textarea($field, $input_atts) {

		$text = '';

		$input_atts['value'] = form_value($input_atts['name'], isset($input_atts['value']) ? $input_atts['value'] : '', $this->form_method);

		$text = $input_atts['value'];
		unset($input_atts['value']);

		return form_textarea($text, $input_atts);
	}

	protected function render_checkbox($field, $input_atts) {
		return $this->render_radio($field, $input_atts);
	}

	protected function render_radio($field, $input_atts) {

		$element = '';

		$checked = array();
		$values = array();

		$checked = $this->is_posted() ? form_value($input_atts['name'], array(), $this->form_method) :
				(isset($input_atts['value']) ? $input_atts['value'] : array());

		$checked = $this->normalize_values($checked);

		if(isset($input_atts['list'])) {
			$values = $this->normalize_values($input_atts['list']);
			unset($input_atts['list']);
		}

		unset($input_atts['value']);

		foreach($values as $key_value => $title) {
			$id = $input_atts['id'] . '_' . $this->normalize_attr($key_value);

			$arr = $input_atts;
			$arr['value'] = $key_value;
			$arr['id'] = $id;

			if($input_atts['type'] == 'checkbox')
				$arr['name'] = $input_atts['name'] . '[]';

			$helper_func = "form_{$input_atts['type']}";

			if(!empty($checked) && in_array($arr['value'], $checked))
				$arr['checked'] = 'checked';

			$item = $helper_func($arr);
			$item = form_label($item . $title, array('for' => $id));
			$element .= $item;
		}

		return $element;
	}

	protected function render_button($field, $input_atts) {
		unset($input_atts['name']);
		return form_button($input_atts);
	}

	protected function render_reset($field, $input_atts) {
		unset($input_atts['name']);
		return form_reset($input_atts);
	}

	protected function render_submit($field, $input_atts) {
		unset($input_atts['name']);
		return form_submit($input_atts);
	}

	protected function render_select($field, $input_atts) {
		return $this->_render_select($field, $input_atts);
	}

	protected function render_multiselect($field, $input_atts) {
		return $this->_render_select($field, $input_atts, true);
	}

	protected function _render_select($field, $input_atts, $is_multi = false) {

		$selected = array();
		$options = array();

		$selected = $this->is_posted() ? form_value($input_atts['name'], array(), $this->form_method) :
				(isset($input_atts['value']) ? $input_atts['value'] : array());

		$selected = $this->normalize_values($selected);

		if(isset($input_atts['list'])) {
			$options = $this->normalize_values($input_atts['list']);
			unset($input_atts['list']);
		}

		unset($input_atts['type'], $input_atts['value']);

		$helper_func = 'form_select';

		if($is_multi) {
			$helper_func = 'form_multiselect';
			$input_atts['name'] .= '[]';
		}

		return $helper_func($input_atts, $options, $selected);
	}

	protected function class_attr_value($new_class, $current_class) {
		
		if(!empty($new_class)) {

			if(!empty($new_class) && strstr($current_class, $new_class) === false) {
				
				if(!empty($current_class) && strpos($current_class, ' ') !== 0)
					$current_class = ' ' . $current_class;

				$current_class = $new_class . $current_class;
			}
		}

		return $current_class;
	}

	protected function render_element($form_path, $field, $options) {

		$element = '';
		$div_atts = array();
		$input_atts = array();

		$defaults = array(
				'label' => false,
				'caption' => false,
				'set_size' => 0,
				'elements' => array(),
				'legend' => '',
				'validation' => array(),
				'extra' => '',
				'after' => '',
				'before' => ''
		);

		extract($defaults);

		$atts = _::parse_args($options, $defaults);

		foreach($atts as $key => $val) {
			if(in_array($key, array('type', 'value', 'list'))) {
				$input_atts[$key] = $val;
			}
			else if(isset($defaults[$key])) {
				$$key = $val;
			}
			else if(substr($key, 0, 8) == 'element_' && strlen($key) > 8) {
				$input_atts[substr($key, 8)] = $val;
			}
			else {
				$div_atts[$key] = $val;
			}
		}

		$form_rel_name = $field;
		if(!empty($form_path))
			$form_rel_name = $form_path . '[' . $form_rel_name . ']';
		
		$input_atts['name'] = array_key_exists('element_name',$options)
			? $options['element_name']
			: $this->get_real_fieldname($form_rel_name);
		$input_atts['id'] = array_key_exists('element_id',$options)
			? $options['element_id']
			: $this->normalize_attr($input_atts['name']);

		// if input type is not set so use text type as default
		if(!isset($input_atts['type']))
			$input_atts['type'] = 'text';

		// add default classes to elements
		$def_divclass = $this->default_class('_div');
		$def_elclass = $this->default_class($input_atts['type']);

		if(!empty($def_elclass))
			$input_atts['class'] = $this->class_attr_value($def_elclass, isset($input_atts['class']) ? $input_atts['class'] : '');

		if(!empty($def_divclass))
			$div_atts['class'] = $this->class_attr_value($def_divclass, isset($div_atts['class']) ? $div_atts['class'] : '');

		$inline_error = $this->inline_errors ? $this->validator->format_errors('', $form_rel_name) : '';

		switch($input_atts['type']) {

			default:

				$method_name = 'render_' . $input_atts['type'];

				if(method_exists($this, $method_name)) {
					$cb = array($this, $method_name);
					$element .= call_user_func($cb, $field, $input_atts);
				}

				break;

			case 'set':

				unset($input_atts['type']);

				if(isset($set_size)) {
					for($i = 0; $i < $set_size; $i++) {
						$keys = array_keys($elements);
						for($j = 0; $j < sizeof($elements); $j++) {
							$element .= $this->render_element($form_rel_name . '[' . $keys[$j] . ']', $i, $elements[$keys[$j]][$i]);
						}
					}
				}

				break;

			case 'array':

				unset($input_atts['type'], $input_atts['name']);

				$element .= $this->render_elements($form_rel_name, $elements);

				break;

			case 'fieldset':

				unset($input_atts['type'], $input_atts['name']);

				$element .= form_fieldset($legend, $input_atts);
				$element .= $this->render_elements($form_rel_name, $elements);
				$element .= form_fieldset_close();

				break;
		}

		if($inline_error) {
			$div_atts['class'] .= (strrpos($div_atts['class'], ' ') == strlen($div_atts['class']) ? '' : ' ') . 'validation-error';
		}

		$out = $this->field_template;
		$attributes = atts_to_string($div_atts);

		// generic label-before-control generator
		if($label !== false)
			$element = form_label($label, array('for' => $input_atts['id'])) . $element;

		$out = str_replace(array(
				':divatts', ':before', ':after', ':extra', ':inline-error', ':element'),
				compact('attributes', 'before', 'after', 'extra', 'inline_error', 'element'), $out);

		return $out;

	}

	public function display($submit_text = false, $atts = array()) {

		$form = '';
		$this->multipart = false;

		$atts = _::parse_args($atts, array('method' => $this->form_method));

		if(!empty($this->nonce_action)) {
			$atts['id'] = $this->form_prefix;
			$form .= form_security($this->nonce_action);
		}

		// compile form if it hasn't been compiled before
		if(!$this->compiled)
			$this->compile();

		$fields = $this->fields;
		$buttons = $this->buttons;

		// default submit button
		if($submit_text !== false) {
			$buttons[] = array(
					'type' => 'submit',
					'class' => 'form-field',
					'element_class' => 'button',
					'value' => $submit_text
			);
		}

		// merge buttons into the fields array to display it at the bottom
		if(!empty($buttons))
			$fields = array_merge($fields, $buttons);

		$elements_html = $this->render_elements('', $fields);

		$form_function = 'form_open';
		if($this->multipart) {
			$form_function .= '_multipart';
		}

		$form = form_open($this->url, $atts) . $form;
		$form .= $elements_html;
		$form .= form_close();

		return $form;

	}

	public function set_lists( $a, $template = null )
	{
		foreach( $a as $k => $v )
		{
			$this ->setList( $k, $v, $template );
		}
	}



	/**
	 * Back-compatibility
	 */
	public function setData( $data )
	{
		$this->set_values($data);
		// throw new Exception('setData method is disabled thing');
	}

	public function setLists( $a, $template = null )
	{
		return $this ->set_lists( $a, $template );
	}

	public function setList( $var, $list, $template = null, $deeper = false )
	{
		if( !$template ) $template = "{title}";
		
		$this ->bind_data($var, $this ->setList_operate( $list, $template, $deeper ));
	}

	// handler
	private function setList_operate( $list, $template, $deeper = false )
	{
		$r = array( );

		foreach( $list as $k => $i )
		{
			// gg
			if( $deeper && is_array( $i ) && !isset( $i['id'] ) )
			{
				$r[$k] = $this ->setList_operate( $i, $template );
				continue;
			}

			// classic
			$id = is_array( $i ) && isset( $i['id'] ) ? $i['id'] : $k;

			$title = !is_array( $i )
				? $i
				: ( str_replace(
				array_map( create_function('$v','return "{{$v}}";'), array_keys( $i ) ),
				array_map( 'strval', array_values( $i ) ),
				(string)($template) ) );
			
			$r[$id] = $title;
		}

		return $r;
	}

	public function getList( $var )
	{
		if( !isset( $this->lists[$var] ) )
			$this ->setList( $var, explode(',',$var) );

		return $this->lists[$var];
	}



	public function setOptions( $a )
	{
		return $this ->set_options( $a );
	}

	public function setOption( $k, $v )
	{
		return $this ->set_option( $k, $v );
	}

	public function getOptions( )
	{
		return $this ->options( 'array' );
	}




	
	/**
	 * Compatibility
	 *
	 * @param string $json
	 * @return this
	 */
	public function set_fieldset( $json )
	{
		// direct call if array is passed
		if(is_array($json)) {
			$this ->set_fields( $json );
			return $this;
		}
		
		$fields = json::decode( $json );
		
		if( empty( $fields ) ) return;
		
		foreach( $fields as $name => $field )
		{
			if( !isset( $field['validation'] ) || !is_array( $field['validation'] ) )
				$field['validation'] = array( );

			if( !isset( $field['element_class'] ) || !is_string( $field['element_class'] ) )
				$field['element_class'] = '';

			switch( $field['type'] )
			{
				case 'static':
					$field['type'] = 'label';
					$field['class'] = 'static-field';
					break;
				case 'header':
					$field['type'] = 'label';
					$field['class'] = 'header-field';
					break;
				case 'text':
					$field['type'] = 'textarea';
					break;
				case 'list':
					$field['type'] = 'select';
				case 'radio':
					$field['list'] = is_string($field['values']) && strpos(',',$field['values'])
						? explode(',',$field['list'])
						: ':'.$field['values'];
					unset( $field['values'] );
					break;
				case 'date':
				case 'month':
					if( isset($field['period']) && $field['period'] )
					{
						unset( $field['period'] );
						$field['type'] .= '_period';
						/*$field_from = $field_to = $field;
						unset( $field_to['label'] );
						$field = array( 'type' => 'array', 'elements' => array(
							'from' => $field_from,
							'to' => $field_to
							) );
						var_dump( $field );*/
					}
					else
					{
						$field['type'] = 'text';
						$field['element_class'] = $field['type'] . 'box';
						$field['validation'][] = $field['type'];
					}
					break;
				default:
					$field['type'] = 'text';
			}
			
			if( isset( $field['required'] ) && $field['required'] )
			{
				array_unshift($field['validation'], 'required');
				unset($field['required']);
			}

			if( isset( $field['null'] ) && $field['null'] )
			{
				// todo: ...
				unset($field['null']);
			}
			
			$fields[$name] = $field;
		}


		$result = array( );
		foreach( $fields as $k => $field )
		if( strpos($k,'[') !== false )
		{
			$nameparts = explode( '[', str_replace( ']', '', $k ) );
			$name = array_shift($nameparts);
			$last = array_pop($nameparts);
			array_traverse_set( $name.'[type]', $result, 'array' );
			
			$name .= '[elements]';
			foreach( $nameparts as $namepart )
			{
				$name .= '['.$namepart.']';
				array_traverse_set( $name.'[type]', $result, 'array' );
				$name .= '[elements]';
			}
			$name .= '['.$last.']';
			array_traverse_set( $name, $result, $field );
		}
		else
		{
			$result[$k] = $field;
		}
		
		//var_dump( $json, $fields );
		$this ->set_fields( $fields );

		return $this;
	}

	// cast lol[wtf][zoo] strings to :array type with keys
	public function array_traverse2(&$array, $name, $value)
	{
		$v = array();

		if(is_string($name))
			parse_str($name, $v);
		
		if(!is_array($array) || !is_array($v))
			return;


		$ref =& $array;

		while(is_array($v)) {
			$first_key = key($v);

			if(is_null($first_key) || !isset($ref[$first_key]))
			{
				$ref[$first_key] = array( );
			}

			$ref =& $ref[$first_key];
			$v = $v[$first_key];
		}
		
		$ref = $value;
	}
}

/* End of file form.php */
/* Location: ./system/libraries/form.php */